// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats;

import bats.map.Blocks;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.BlenderKey;
import com.jme3.export.JmeExporter;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.system.JmeContext;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import jme3utilities.Misc;
import jme3utilities.MyString;
import jme3utilities.debug.Printer;

/**
 * A headless simple application to build or remove converted assets (J3O files
 * and JPEG files) used by the Bats Game.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class AssetProcessor
        extends SimpleApplication {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(AssetProcessor.class.getName());
    /**
     * application name for the usage message
     */
    final private static String applicationName = "AssetProcessor";
    // *************************************************************************
    // fields
    /**
     * false means skip optional build targets, true means process all targets
     */
    @Parameter(names = {"-a", "--all"},
            description = "process all build targets")
    private static boolean allFlag = false;
    /**
     * false means convert, true means remove all build targets
     */
    @Parameter(names = {"--clean", "--cleanup"},
            description = "remove build targets")
    private static boolean cleanupFlag = false;
    /**
     * true means just display the usage message; false means run the
     * application
     */
    @Parameter(names = {"-h", "-u", "--help", "--usage"}, help = true,
            description = "display this usage message")
    private static boolean usageOnly = false;
    // *************************************************************************
    // new methods exposed

    /**
     * Main entry point for the Bats Game asset builder.
     *
     * @param arguments array of command-line arguments (not null)
     */
    public static void main(String[] arguments) {
        /*
         * Mute the chatty loggers found in some imported packages.
         */
        Misc.setLoggingLevels(Level.WARNING);
        /*
         * Set the logging level for this class.
         */
        logger.setLevel(Level.INFO);
        /*
         * Instantiate the application.
         */
        AssetProcessor application = new AssetProcessor();
        /*
         * Parse the command-line arguments.
         */
        JCommander jCommander = new JCommander(application, arguments);
        jCommander.setProgramName(applicationName);
        if (usageOnly) {
            jCommander.usage();
            return;
        }
        application.start(JmeContext.Type.Headless);
        /*
         * ... and onward to AssetProcessor.simpleInitApp()!
         */
    }
    // *************************************************************************
    // SimpleApplication methods

    /**
     * Initialize this application.
     */
    @Override
    public void simpleInitApp() {
        String userDir = System.getProperty("user.dir");
        logger.log(Level.INFO, "working directory is {0}",
                MyString.quote(userDir));
        /*
         * textures
         */
        processBlockTextures();
        processTexture("terrain/height/basin");
        processUnitTextures();
        /*
         * models
         */
        processBlockModels();
        processIndicatorModels();
        processModel("missiles/shafts/pilum");
        processPropModels();
        processUnitModels();

        stop();
    }
    // *************************************************************************
    // private methods

    /**
     * Delete a target asset if it exists.
     *
     * @param path (not null)
     * @param file (not null)
     */
    private void cleanup(String path, File file) {
        assert path != null;

        if (!file.exists()) {
            logger.log(Level.INFO, "asset {0} not found",
                    MyString.quote(path));
            return;
        }
        /*
         * Delete the asset.
         */
        boolean success = file.delete();
        if (success) {
            logger.log(Level.INFO, "deleted asset {0}",
                    MyString.quote(path));
        } else {
            logger.log(Level.SEVERE, "failed to delete asset {0}",
                    MyString.quote(path));
            throw new RuntimeException();
        }
    }

    /**
     * Process models in the "Models/blocks" subfolder.
     */
    private void processBlockModels() {
        String[] paths = {
            "arcade", "corridor", "corridorPavement", "gateway", "ground",
            "ground2x2", "ground4x4", "ladder", "merlonLeft", "merlonRight",
            "parapet", "pavement", "plain", "plain8x8", "platform", "pond",
            "pool", "stairwayLeft", "stairwayRight", "tower/base",
            "tower/battlement", "tower/entry", "tower/pavement"
        };
        processModelFolder("blocks", paths);
    }

    /**
     * Process textures in the "Textures/blocks" subfolder.
     */
    private void processBlockTextures() {
        String[] names = {
            "bronze", "dirt", "pavers", "stone", "tile", "wood"
        };
        processTextureFolder("blocks", names);
    }

    /**
     * Process textures in the "Models/indicators" subfolder.
     */
    private void processIndicatorModels() {
        String[] paths = {
            "3d cursor", "pick", "target", "viewpoint"
        };
        processModelFolder("indicators", paths);
    }

    /**
     * If cleanupFlag is false, convert a Blender model asset to J3O format. If
     * cleanupFlag is true, remove the J3O target.
     *
     * @param path (not null)
     */
    private void processModel(String path) {
        assert path != null;
        logger.log(Level.FINE, "path={0}", path);

        String targetFilePath = String.format("assets/Models/%s.j3o", path);
        File targetFile = new File(targetFilePath);

        if (cleanupFlag) {
            cleanup(targetFilePath, targetFile);
            return;
        }
        String sourceAssetPath = String.format("Models/%s.blend", path);
        /*
         * Load the Blender model.
         */
        BlenderKey key = new BlenderKey(sourceAssetPath);
        switch (path) {
            case "units/bob/bob":
                key.addSkeletonAnimation("bob", "balanced-neutral");
                key.addSkeletonAnimation("bob", "left-lift");
                key.addSkeletonAnimation("bob", "left-trail");
                key.addSkeletonAnimation("bob", "right-lift");
                key.addSkeletonAnimation("bob", "right-trail");
                break;

            case "units/nan/nan":
                key.addSkeletonAnimation("nan", "balanced-neutral");
                key.addSkeletonAnimation("nan", "left-lift");
                key.addSkeletonAnimation("nan", "left-trail");
                key.addSkeletonAnimation("nan", "right-lift");
                key.addSkeletonAnimation("nan", "right-trail");
        }
        Spatial model = assetManager.loadModel(key);
        logger.log(Level.INFO, "read Blender asset {0}",
                MyString.quote(sourceAssetPath));
        validateSpatial(model);

        new Printer().printSubtree(model);
        /*
         * Save the model in J3O format.
         */
        JmeExporter exporter = BinaryExporter.getInstance();
        try {
            exporter.save(model, targetFile);
        } catch (IOException exception) {
            logger.log(Level.SEVERE, "write to {0} failed",
                    MyString.quote(targetFilePath));
            throw new RuntimeException();
        }
        logger.log(Level.INFO, "wrote file {0}",
                MyString.quote(targetFilePath));
    }

    private void processModelFolder(String folderPath, String[] paths) {
        for (String modelPath : paths) {
            String modelType = Blocks.getType(modelPath);
            String assetPath = String.format("%s/%s/%s",
                    folderPath, modelPath, modelType);
            processModel(assetPath);
        }
    }

    /**
     * Process models in the "Models/props" subfolder.
     */
    private void processPropModels() {
        String[] paths = {
            "barrel", "mantlet"
        };
        processModelFolder("props", paths);
    }

    /**
     * If cleanupFlag is false, convert a PNG texture asset to JPEG format. If
     * cleanupFlag is true, remove the JPEG target.
     *
     * @param path (not null)
     */
    private void processTexture(String path) {
        assert path != null;
        logger.log(Level.FINE, "path={0}", path);

        String targetFilePath = String.format("assets/Textures/%s.jpg", path);
        File targetFile = new File(targetFilePath);

        if (cleanupFlag) {
            cleanup(targetFilePath, targetFile);
            return;
        }

        String sourceFilePath = String.format("assets/Textures/%s.png", path);
        File sourceFile = new File(sourceFilePath);
        /*
         * Load the PNG texture.
         */
        BufferedImage image = null;
        try {
            image = ImageIO.read(sourceFile);
        } catch (IOException exception) {
            logger.log(Level.SEVERE, "failed to read {0}", path);
            throw new RuntimeException();
        }
        /*
         * Save the texture in JPEG format.
         */
        try {
            ImageIO.write(image, "jpg", targetFile);
        } catch (IOException exception) {
            logger.log(Level.SEVERE, "failed to write {0}", targetFilePath);
            throw new RuntimeException();
        }
    }

    private void processTextureFolder(String folderPath, String[] names) {
        for (String name : names) {
            String texturePath = String.format("%s/%s", folderPath, name);
            processTexture(texturePath);
        }
    }

    /**
     * Process models in the "Models/units" subfolder.
     */
    private void processUnitModels() {
        processModel("units/bob/bob");
        if (allFlag) {
            processModel("units/nan/nan");
        }
    }

    /**
     * Process textures in the "Textures/units" subfolder.
     */
    private void processUnitTextures() {
        processTexture("units/bob/bob-blend");
        if (allFlag) {
            processTexture("units/nan/nan-bc");
        }
    }

    /**
     * Validate a geometry which is part of an imported model.
     *
     * @param geometry (not null)
     */
    private void validateGeometry(Geometry geometry) {
        Mesh mesh = geometry.getMesh();
        Mode mode = mesh.getMode();
        if (mode != Mesh.Mode.Triangles) {
            throw new AssetLoadException("expected a mesh in Triangles mode");
        }
    }

    /**
     * Validate a spatial which is part of an imported model.
     *
     * @param spatial (not null)
     */
    private void validateSpatial(Spatial spatial) {
        if (spatial instanceof Geometry) {
            Geometry geometry = (Geometry) spatial;
            validateGeometry(geometry);
            return;
        }
        assert spatial instanceof Node;
        Node node = (Node) spatial;
        for (Spatial child : node.getChildren()) {
            validateSpatial(child);
        }
    }
}